﻿using System;
using System.Configuration;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Hosting;
using EPiServer.Configuration;
using EPiServer.Web.Hosting;
using Encoder = System.Drawing.Imaging.Encoder;

namespace TemplateFoundation.Handlers.DynamicImageProcessor
{
    /// <summary>
    /// Dynamic image processor used to resize and/or crop images
    /// </summary>
    public sealed class Processor : IHttpHandler
    {
        ProcessorConfig _config;
        string _mimeType = "";
        string _cacheFile = "";
        private bool _useWebConfig = true;

        bool IHttpHandler.IsReusable
        {
            get { return false; }
        }

        string GetCacheFileName(string imageUrl, int requestedWidth, int requestedHeight, int cropWidth, int cropHeight, bool keepRatio, int offsetX, int offsetY, bool scaleUp)
        {
            var sb = new StringBuilder();

            var fileExtension = Path.GetExtension(imageUrl);

            sb.Append(_useWebConfig ? _config.Cache.CacheDirectory : ProcessorDefaultConfig.CacheDirectory); // Add cache folder path

            sb.Append(imageUrl.Replace("/", "\\").Replace(fileExtension, string.Empty)); // Add image path to cache base path, excluding the file extension

            if (requestedHeight>0)
            {
                sb.AppendFormat("{0}.", requestedHeight);
            }

            if (requestedWidth>0)
            {
                sb.AppendFormat("{0}.", requestedWidth);
            }

            if (keepRatio && requestedHeight>0 && requestedWidth>0)
            {
                sb.Append("skewed.");
            }

            if (cropHeight>0)
            {
                sb.AppendFormat("ch-{0}.", cropHeight);
            }

            if (cropWidth>0)
            {
                sb.AppendFormat("cw-{0}.", cropWidth);
            }

            if(offsetX > -1)
            {
                sb.AppendFormat("ox-{0}", offsetX);
            }

            if(offsetY > -1)
            {
                sb.AppendFormat("oy-{0}", offsetY);
            }

            if (scaleUp)
            {
                sb.Append("scaleUp");
            }

            sb.Append(fileExtension);

            return sb.ToString();
        }

        void IHttpHandler.ProcessRequest(HttpContext context)
        {
            // load the configuration from web._config
            _config = (ProcessorConfig)ConfigurationManager.GetSection("DynamicImageProcessor");

            if (_config==null) // No configuration section in web app, use default configuration instead
            {
                _useWebConfig = false;
            }
           
            var useCache = _useWebConfig ? _config.Cache.Enabled : ProcessorDefaultConfig.CacheEnabled;

            var highQualityRendering = _useWebConfig ? _config.HighQuality : ProcessorDefaultConfig.HighQuality;

            // Setup the Response, Clear out all the fun stuff
            context.Response.Clear();
            context.Response.BufferOutput = true;

            // get the orginally requested url and the attached height and width request. Then set to 0 if not present
            var imageUrl = context.Request.Path;

            var widthParameterName = context.Request.QueryString["w"]!=null ? "w" : "width";
            var heightParameterName = context.Request.QueryString["h"]!=null ? "h" : "height";

            var imageHeight = (context.Request.QueryString[heightParameterName] != null ? Convert.ToInt32(context.Request.QueryString[heightParameterName]) : 0);
            var imageWidth = (context.Request.QueryString[widthParameterName] != null ? Convert.ToInt32(context.Request.QueryString[widthParameterName]) : 0);
            var keepRatio = !(context.Request.QueryString["r"] ?? string.Empty).Equals("false", StringComparison.OrdinalIgnoreCase); // Default is true, aspect ratio is kept
            var cropHeight = (context.Request.QueryString["ch"] != null ? Convert.ToInt32(context.Request.QueryString["ch"]) : 0);
            var cropWidth = (context.Request.QueryString["cw"] != null ? Convert.ToInt32(context.Request.QueryString["cw"]) : 0);

            var offsetX = (context.Request.QueryString["ox"] != null ? Convert.ToInt32(context.Request.QueryString["ox"]) : -1);
            var offsetY = (context.Request.QueryString["oy"] != null ? Convert.ToInt32(context.Request.QueryString["oy"]) : -1);

            var scaleUp = context.Request.QueryString["up"] != null;

            var widthSpecified = imageWidth > 0;
            var heightSpecified = imageHeight > 0;
            var cropWidthSpecified = cropWidth > 0;
            var cropHeightSpecified = cropHeight > 0;

            // Determine suitable MIME type based on file extension
            var fileExtension = Path.GetExtension(imageUrl);
            switch (fileExtension)
            {
                case ".jpg":
                case ".jpeg":
                    _mimeType = "image/jpeg";
                    break;
                case ".png":
                    _mimeType = "image/png";
                    break;
                case ".gif":
                    _mimeType = "image/gif";
                    break;
                default: // Otherwise use default from config
                    _mimeType = _useWebConfig ? _config.DefaultMimeType : ProcessorDefaultConfig.DefaultMimeType;
                    break;
            }

            // lets check and see if we've already used this resolution before
            if (useCache)
            {
                _cacheFile = GetCacheFileName(imageUrl, imageWidth, imageHeight, cropWidth, cropHeight, keepRatio, offsetX, offsetY, scaleUp);

                if (imageHeight>0 || imageWidth>0)
                {
                    if (File.Exists(_cacheFile))
                    {
                        var info = new FileInfo(_cacheFile);
                        SetCachePolicy(context, info.LastWriteTimeUtc);
                        if (!NotModifiedHandling(context, info.LastWriteTime))
                        {
                            // sends the previously generated image
                            context.Response.ContentType = _mimeType;
                            context.Response.WriteFile(_cacheFile);
                            context.Response.Flush();
                        }

                        context.Response.End();
                        return;
                    }
                }
            }

            // The Original Image object
            Image imageFullSize;

            var physicalFile = HostingEnvironment.VirtualPathProvider.GetFile(imageUrl);

            var physicalPath = physicalFile as UnifiedFile != null
                                      ? ((UnifiedFile)physicalFile).LocalPath
                                      : context.Server.MapPath(imageUrl);

            // Lets see if the requested file even exists (if the LocalPath property is null the VPP may serve content from an off-site location)
            if (!string.IsNullOrEmpty(physicalPath) && !File.Exists(physicalPath))
            {
                // it doesn't, so lets see if were using a not found image        
                if (_useWebConfig ? _config.EnableImageNotFound : ProcessorDefaultConfig.EnableImageNotFound)
                {
                    // sends the not found image
                    context.Response.ContentType = _mimeType;
                    context.Response.WriteFile(context.Server.MapPath(_useWebConfig ? _config.ImageNotFound.FileName : ProcessorDefaultConfig.NotFoundImage));
                    context.Response.Flush();
                    context.Response.End();
                    return;
                }

                // sends a 404
                context.Response.Clear();
                context.Response.StatusCode = 404;
                context.Response.End();
                return;
            }

            // lets check to see if requesting a custom resolution?
            if (imageHeight != 0 || imageWidth != 0 || cropHeight!=0 || cropWidth!=0)
            {
                // they are, lets get the original image
                if (string.IsNullOrEmpty(physicalPath))
                {
                    using(var fs = physicalFile.Open())
                    {
                        imageFullSize = Image.FromStream(fs);    

                        fs.Close();
                    }
                }
                else
                {
                    imageFullSize = Image.FromFile(physicalPath);    
                }

                // If cropping without resizing
                if ((cropHeight>0 || cropWidth>0) && (imageWidth==0 && imageHeight==0))
                {
                    // No custom size parameters, but crop options specified
                    imageHeight = imageFullSize.Height;
                    imageWidth = imageFullSize.Width;
                }

                // Now lets check if they are requesting an enlarged image (we only do that if explicitly requested with the "up" parameter)
                // Begin beautiful IF-craftmanship...
                var requestingEnlarged = ((imageHeight > imageFullSize.Height && !widthSpecified)
                    || (imageWidth>imageFullSize.Width && !heightSpecified)
                    || (imageHeight > imageFullSize.Height && imageWidth > imageFullSize.Width && widthSpecified && heightSpecified));

                if (requestingEnlarged && !scaleUp) // If so, send original image
                {
                    context.Response.ContentType = _mimeType;

                    if (string.IsNullOrEmpty(physicalPath))
                    {
                        // Read file stream and output it to the response stream
                        using (var fs = physicalFile.Open())
                        {
                            using (var sr = new BinaryReader(fs))
                            {
                                var bytes = sr.ReadBytes((int)fs.Length);

                                context.Response.BinaryWrite(bytes);

                                sr.Close();
                            }

                            fs.Close();
                        }
                    }
                    else
                    {
                        var info = new FileInfo(physicalPath);
                        SetCachePolicy(context, info.LastWriteTimeUtc);
                        if (!NotModifiedHandling(context, info.LastWriteTime))
                        {
                            context.Response.WriteFile(physicalPath);
                        }
                    }

                    context.Response.Flush();
                    context.Response.End();

                    imageFullSize.Dispose();

                    return;
                }

                // now we do support aspect ration based request, so they can only change height or width, 
                // and we'll do the calculation so lets do the calculation. 

                double aspectratio = Convert.ToDouble(Convert.ToDouble(imageFullSize.Width) / Convert.ToDouble(imageFullSize.Height));

                if (imageHeight>0 && imageWidth>0 && keepRatio)
                {
                    // Both dimensions specified, keep aspect ratio and stay within boundaries
                    int wouldBeHeight = Convert.ToInt32(imageWidth / aspectratio);
                    int wouldBeWidth = Convert.ToInt32(imageHeight  * aspectratio);

                    if (wouldBeHeight>imageHeight) // Going by width would result in an image being to high
                    {
                        imageWidth = Convert.ToInt32(imageHeight*aspectratio);
                    }
                    else if (wouldBeWidth>imageWidth) // Going by height would result in an image being to wide
                    {
                        imageHeight = Convert.ToInt32(imageWidth / aspectratio);
                    }
                }
                else
                {
                    // If either height or width is left out the image will be scaled according to the dimension (height or width) specified
                    if (imageHeight == 0)
                    {
                        imageHeight = Convert.ToInt32(imageWidth / aspectratio);
                    }
                    else if (imageWidth == 0)
                    {
                        imageWidth = Convert.ToInt32(imageHeight  * aspectratio);
                    }
                }

                // ok now we have the requested dimensions. lets check to see if we've already generated their request. 
                if (useCache)
                {
                    _cacheFile = GetCacheFileName(imageUrl, widthSpecified ? imageWidth : 0, heightSpecified ? imageHeight : 0, cropWidthSpecified ? cropWidth : 0, cropHeightSpecified ? cropHeight : 0, keepRatio, offsetX, offsetY, scaleUp);

                    if (File.Exists(_cacheFile))
                    {
                        // we have, lets send that one, no need to regenerate a file we've already generated. 
                        context.Response.ContentType = _mimeType;
                        var info = new FileInfo(_cacheFile);
                        SetCachePolicy(context, info.LastWriteTimeUtc);
                        if (!NotModifiedHandling(context, info.LastWriteTime))
                        {

                            context.Response.WriteFile(_cacheFile);
                            context.Response.Flush();
                        }
                        context.Response.End();

                        imageFullSize.Dispose();

                        return;

                    }
                }

                // ok now comes the dog and pony show, let's resize the image        
                var mybmp = new Bitmap(imageWidth, imageHeight);
                var objGraphics = Graphics.FromImage(mybmp);

                objGraphics.CompositingMode = CompositingMode.SourceOver;
                objGraphics.CompositingQuality = highQualityRendering ? CompositingQuality.GammaCorrected : CompositingQuality.Default;

                objGraphics.PixelOffsetMode = highQualityRendering ? PixelOffsetMode.HighQuality : PixelOffsetMode.Default;

                objGraphics.InterpolationMode = highQualityRendering ? InterpolationMode.HighQualityBicubic : InterpolationMode.Default;
                objGraphics.SmoothingMode = highQualityRendering ? SmoothingMode.HighQuality : SmoothingMode.Default;

                objGraphics.DrawImage(imageFullSize, 0, 0, mybmp.Width, mybmp.Height);

                var qualityEncoder = Encoder.Quality;
                var colorEncoder = Encoder.ColorDepth;

                var ratio = new EncoderParameter(qualityEncoder, highQualityRendering ? 100L : 80L);
                var light = new EncoderParameter(colorEncoder, 24L);
                var codecParameters = new EncoderParameters(2);
                codecParameters.Param[0] = ratio;
                codecParameters.Param[1] = light;

                const float brightness = 0;
                var bMatrix = new ColorMatrix(new[]
                                                  {
                                                      new Single[] {1, 0, 0, 0, 0},
                                                      new Single[] {0, 1, 0, 0, 0},
                                                      new Single[] {0, 0, 1, 0, 0},
                                                      new Single[] {0, 0, 0, 1, 0},
                                                      new[] {brightness, brightness, brightness, 0, 1}
                                                  });

                ApplyColorMatrix(ref mybmp, bMatrix);

                // Crop if crop height has been specified
                if (cropHeight > 0 && cropHeight < mybmp.Height)
                {
                    var heightDifference = mybmp.Height - cropHeight;
                    Rectangle rect;

                    if (offsetY > -1)
                    {
                        rect = new Rectangle(0, offsetY, mybmp.Width, cropHeight);
                    }
                    else
                    {
                        rect = new Rectangle(0, heightDifference/2, mybmp.Width, cropHeight);
                    }

                    var cropped = mybmp.Clone(rect, mybmp.PixelFormat);
                    mybmp = cropped;
                }

                // Crop if crop width has been specified
                if (cropWidth > 0 && cropWidth < mybmp.Width)
                {
                    var widthDifference = mybmp.Width - cropWidth;
                    Rectangle rect;

                    if (offsetX > -1)
                    {
                        rect = new Rectangle(offsetX, 0, cropWidth, mybmp.Height);
                    }
                    else
                    {
                        rect = new Rectangle(widthDifference/2, 0, cropWidth, mybmp.Height);
                    }

                    var cropped = mybmp.Clone(rect, mybmp.PixelFormat);
                    mybmp = cropped;
                }

                SetCachePolicy(context, DateTime.UtcNow);
                if (!NotModifiedHandling(context, DateTime.Now))
                {
                    if (_mimeType == "image/png")
                        // Transparent PNG can only be written to seekable stream, using an intermediary memory stream
                    {
                        using (var ms = new MemoryStream())
                        {
                            mybmp.Save(ms, GetEncoderInfo(_mimeType), codecParameters);

                            ms.WriteTo(context.Response.OutputStream);

                            ms.Close();
                        }
                    }
                    else
                    {
                        // Write image directly to output stream
                        mybmp.Save(context.Response.OutputStream, GetEncoderInfo(_mimeType), codecParameters);
                    }

                    if (useCache)
                    {
                        // check to make sure the cache directory exists
                        if (!Directory.Exists(_cacheFile.Substring(0, _cacheFile.LastIndexOf("\\"))))
                            Directory.CreateDirectory(_cacheFile.Substring(0, _cacheFile.LastIndexOf("\\")));
                        // save the image to the cache directory
                        mybmp.Save(_cacheFile, GetEncoderInfo(_mimeType), codecParameters);
                    }
                    objGraphics.Dispose();
                    mybmp.Dispose();

                    // send it back to the client
                    context.Response.ContentType = _mimeType;
                    context.Response.Flush();
                }

                // Dispose objects
                imageFullSize.Dispose();
                objGraphics.Dispose();
                mybmp.Dispose();

                context.Response.End();

                return;

            }

            // if the file is there and no height or width changes are required. 
            // send the original file
            context.Response.ContentType = _mimeType;
            context.Response.ContentEncoding = Encoding.UTF8;
            if (string.IsNullOrEmpty(physicalPath))
            {
                // Read file stream and output it to the response stream
                using (var fs = physicalFile.Open())
                {
                    using (var sr = new BinaryReader(fs))
                    {
                        var bytes = sr.ReadBytes((int) fs.Length);

                        context.Response.BinaryWrite(bytes);

                        sr.Close();
                    }

                    fs.Close();
                }
            }
            else
            {
                var info = new FileInfo(physicalPath);
                SetCachePolicy(context, info.LastWriteTimeUtc);
                if (!NotModifiedHandling(context, info.LastWriteTime))
                {
                    context.Response.WriteFile(physicalPath);
                }

            }
            
            context.Response.Flush();
            context.Response.End();
        }

        static bool CheckIsModifiedAndAddETag(HttpContext context, DateTime modifiedDate)
        {
            DateTime time;
            string s = context.Request.Headers["If-Modified-Since"];
            context.Response.AppendHeader("ETag", "\"" + modifiedDate.ToFileTime().ToString("X8", CultureInfo.InvariantCulture) + "\"");
            return (!DateTime.TryParse(s, out time) || (modifiedDate > time.AddSeconds(1.0)));
        }

        private bool NotModifiedHandling(HttpContext context, DateTime modifiedDate)
        {
            if (!CheckIsModifiedAndAddETag(context, modifiedDate))
            {
                context.Response.AddHeader("Content-Length", "0");
                context.Response.StatusCode = 304;
                return true;
            }
            return false;
        }

        private void SetCachePolicy(HttpContext context, DateTime fileChangedDate)
        {
            if(fileChangedDate > DateTime.UtcNow)
            {
                fileChangedDate = DateTime.UtcNow;
            }

            context.Response.Cache.SetLastModified(fileChangedDate);
            context.Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(60));
            context.Response.Cache.SetValidUntilExpires(true);
            context.Response.Cache.VaryByParams.IgnoreParams = true;
            context.Response.Cache.SetOmitVaryStar(true);
        }

        private static void ApplyColorMatrix(ref Bitmap myImage, ColorMatrix myMatrix)
        {
            var imgAtt = new ImageAttributes();
            var bmpMatrix = new Bitmap(myImage.Width, myImage.Height);
            var grMatrix = Graphics.FromImage(bmpMatrix);

            imgAtt.SetColorMatrix(myMatrix);

            grMatrix.DrawImage(myImage, new Rectangle(0, 0, myImage.Width, myImage.Height), 0, 0, myImage.Width, myImage.Height, GraphicsUnit.Pixel, imgAtt);

            myImage = bmpMatrix;

            grMatrix.Dispose();

            imgAtt.Dispose();
        }

        private static ImageCodecInfo GetEncoderInfo(string mime)
        {
            var encoders = ImageCodecInfo.GetImageEncoders();
            return encoders.FirstOrDefault(e => e.MimeType == mime);
        }
    }

}
